说说react中的 styled-components
在react项目中写CSS大致有以下三种方式:内联方式、引入样式类、使用styled-components。
<div style={{color: '#f3623d', fontSize: 14}}>Menu</div>
<div style={{color: '#f3623d', fontSize: 14}}>Menu</div>
以内联方式书写CSS代码,简单直接,适用于只设置一两个样式书写的情况,编译后生成的代码也是内联在HTML结点的style属性上。这种写法发缺陷是,一旦样式属性设置的过多,就会导致组件代码复杂,不易维护。当然也可以把样式对象从JSX中抽离出去,定义成一个单独的对象,或写在公共模块里,来解决该问题。这又会带来,一旦这样的样式对象过多,组件内部结点的样式优先级不清晰,且不易抽离成公共的样式模块,来供其它组件复用。
render() {
return <span className="menu navigation-menu">Menu</span>
}
render() {
return <span className="menu navigation-menu">Menu</span>
}
通过设置className属性,来给组件设置样式,是另一种常见的方式。对于需要抽离成公共样式的需求,可以定义在一个独立的样式类区块中,达到样式共享的目的,解决内联方式的弊端。样式类,可以定义在style中,也可以写在单独的样式文件中。借助于webpack构建工具的相关插件可以方便地集成less、sass等,做css的优雅降级。缺点是,静态样式类,有可能和页面中其它模块的命名冲突,导致某一处的样式类,污染了全局或其它模块的正常展示。当然,人为去定义一个不易导致冲突的样式类,可以在一定程度上缓解这个状况,但对于大型复杂的多人协作项目,这并不是一个靠谱的解决方案。
.diItemWrap {
width: 100%;
position: relative;
display: inline-block;
margin: 2px 10px 2px 0;
}
.diItemTitle {
display: inline-block;
text-align: right;
padding-right: 10px;
font-weight: 500;
width: 120px;
}
.diItemWrap {
width: 100%;
position: relative;
display: inline-block;
margin: 2px 10px 2px 0;
}
.diItemTitle {
display: inline-block;
text-align: right;
padding-right: 10px;
font-weight: 500;
width: 120px;
}
import styles from './styles.module.css';
return (
<div className={styles.diItemWrap}>
<span className={styles.diItemTitle}>
{uiRequred}{title}
</span>
</div>
);
import styles from './styles.module.css';
return (
<div className={styles.diItemWrap}>
<span className={styles.diItemTitle}>
{uiRequred}{title}
</span>
</div>
);
在项目工程中,借助于webpack的一些列CSS插件,可以实现在 JSX定义的组件中引用外部的样式类,并且最终注入的的样式类可以保证唯一性,不会引起命名冲突。但是不能很好地实现样式的继承,比如本例中只想在外层div标签引用一个样式类而同时控制外层div标签 和 里面的子元素是做不到的。
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
在日常的项目开发中使用styled-components(简称 SC),是做react项目时处理CSS最普遍的方式,它的优势很多,解决了之前在react中书写CSS的痛点,是一种CSS IN JS的成熟解决方案。
优势
styled-components的诞生初衷,致力于增强React组件中设置样式的效果,改善前端开发人员在JSX组件中书写CSS的体验,优化了最终的样式输出。
自动注入样式
styled-components 会自动追踪组件中的样式类,将页面中和组件关联的样式代码最终打包输出,没有引用到的无关样式代码自动忽略。这样结合代码拆分插件和异步组件,可以达到生成的代码量最少。
类名不会冲突
styled-components 给用户定义的样式类做增强处理,保证最终输出的样式类,在页面全局中保持唯一,轻松解决命名冲突的问题。
支持动态样式
根据传递的组件属性 或者 全局的主题,可以实现样式或主题的切换,无需手动维护。
其他优势
有了styled-components,在大型项目中可以轻松维护样式部分代码,无用的CSS自动移除,对于比较新的样式属性支持自动增加前缀来兼容低版本浏览器。
用法简介
安装步骤,自动跳过,使用 NPM 或 yarn 命令安装即可。
引入
import styled from 'styled-components';
import styled from 'styled-components';
普通用法
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
如上所示,定义了两个样式组件 Wrapper 和 Title,这两个业务组件自动绑定了一组样式。 最终生成的代码如下:
<style>
.cHGPOa {
padding: 4em;
background: papayawhip;
}
.ikxKmn {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
</style>
<section class="sc-cBNfnY cHGPOa"><h1 class="sc-gWHgXt ikxKmn">Hello World!</h1></section>
<style>
.cHGPOa {
padding: 4em;
background: papayawhip;
}
.ikxKmn {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
</style>
<section class="sc-cBNfnY cHGPOa"><h1 class="sc-gWHgXt ikxKmn">Hello World!</h1></section>
可以看到,最终输出的两个DOM结点section 和 h1 上自动添加了样式类,样式类关联了组件内设置的样式属性。
动态展示
const Lable = styled.span`
font-size: ${props => props.bigger ? "16px" : "12px"};
`;
render(
<div>
<Lable>正常</Lable>
<Lable bigger>大号</Lable>
</div>
);
const Lable = styled.span`
font-size: ${props => props.bigger ? "16px" : "12px"};
`;
render(
<div>
<Lable>正常</Lable>
<Lable bigger>大号</Lable>
</div>
);
通过在组件的模板字符串中,传递插值函数,根据属性参数来动态改变展示效果。
样式继承
const Button = styled.button`
color: #fe3df6;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
`;
const RoundedButton = styled(Button)`
border: 2px solid #fe3df6;
border-radius: 3px;
`;
render(
<div>
<Button>正常按钮</Button>
<RoundedButton>Tomato Button</RoundedButton>
</div>
);
const Button = styled.button`
color: #fe3df6;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
`;
const RoundedButton = styled(Button)`
border: 2px solid #fe3df6;
border-radius: 3px;
`;
render(
<div>
<Button>正常按钮</Button>
<RoundedButton>Tomato Button</RoundedButton>
</div>
);
首先用普通写法定义一个样式组件Button,通过将样式组件Button包裹在styled模板字符串中,实现对Button组件的样式继承。
装饰组件
const Link = ({ className, children }) => (
<a className={className}>
{children}
</a>
);
const StyledLink = styled(Link)`
color: palevioletred;
font-weight: bold;
`;
render(
<div>
<Link>普通链接</Link>
<br />
<StyledLink>装饰后的链接</StyledLink>
</div>
);
const Link = ({ className, children }) => (
<a className={className}>
{children}
</a>
);
const StyledLink = styled(Link)`
color: palevioletred;
font-weight: bold;
`;
render(
<div>
<Link>普通链接</Link>
<br />
<StyledLink>装饰后的链接</StyledLink>
</div>
);
styled 方法可以作用在自定义 或 第三方组件上,只要组件接收的className属性绑定在了DOM结点上。
传递属性
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: ${props => props.inputColor || "red"};
background: papayawhip;
border: none;
border-radius: 3px;
`;
render(
<div>
<Input defaultValue="红色字体" type="text" />
<Input defaultValue="绿色字体" type="text" inputColor="green" />
</div>
);
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: ${props => props.inputColor || "red"};
background: papayawhip;
border: none;
border-radius: 3px;
`;
render(
<div>
<Input defaultValue="红色字体" type="text" />
<Input defaultValue="绿色字体" type="text" inputColor="green" />
</div>
);
针对普通的DOM结点,传递已知的HTML属性;针对一个React组件,可以传递任何属性。如上面的代码所示,inputColor属性不会作用到原始的input DOM结点上,用于供上层组件Input来做样式的个性化展示。
附加属性
const Input = styled.input.attrs(props => ({
type: "text",
size: props.size || "1em",
}))`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
/* 使用上面定义好的动态计算属性 size */
margin: ${props => props.size};
padding: ${props => props.size};
`;
render(
<div>
<Input placeholder="小输入框" />
<br />
<Input placeholder="大输入框" size="2em" />
</div>
);
const Input = styled.input.attrs(props => ({
type: "text",
size: props.size || "1em",
}))`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
/* 使用上面定义好的动态计算属性 size */
margin: ${props => props.size};
padding: ${props => props.size};
`;
render(
<div>
<Input placeholder="小输入框" />
<br />
<Input placeholder="大输入框" size="2em" />
</div>
);
动画支持
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
// 下面创建一个可以在两秒内 旋转一次的组件
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
render(
<Rotate>< 🐔 ></Rotate>
);
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
// 下面创建一个可以在两秒内 旋转一次的组件
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
render(
<Rotate>< 🐔 ></Rotate>
);
CSS @keyframes 动画并不局限在单个组件中,为了避免命名冲突而污染全局,可以使用 styled-components 中提供的 keyframes 关键字,来创建唯一的动画实例(即保证 rotage的独一无二) 更多用法请移步至文末的官方文档地址
实现原理
在了解 styled-components 之前需要先温习一下 ES6中的 Tagged Template Literals(带标签的模板字符串)
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
var str0 = strings[0]; // "that "
var str1 = strings[1]; // " is a "
var ageStr;
if (ageExp > 99){
ageStr = 'centenarian';
} else {
ageStr = 'youngster';
}
return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
var str0 = strings[0]; // "that "
var str1 = strings[1]; // " is a "
var ageStr;
if (ageExp > 99){
ageStr = 'centenarian';
} else {
ageStr = 'youngster';
}
return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster
如上面代码所示,myTag作为模板字符串的标签,可以视作一个函数,第一个参数包含一个字符串值的数组,其余的参数则一一关联到模板中的表达式。 同理,针对 使用styled-components定义的样式组件。
const Title = styled.h1`
font-size: 1.5em;
`;
// 等价于
const Title = styled.h1('font-size: 1.5em;')
const Title = styled.h1`
font-size: 1.5em;
`;
// 等价于
const Title = styled.h1('font-size: 1.5em;')
首先,引入styled-components时 底层会维护一个计数器counter,每生成一个组件实例,数值就会发生改变,再结合hash函数以及前缀等方式,生成唯一的组件ID。
counter++;
const componentId = 'sc-' + hash('sc' + counter);
counter++;
const componentId = 'sc-' + hash('sc' + counter);
接下来,我们简单分析一下 styled-components 的核心源码(这里我们使用当前最新版本的main分支),入口文件位于[ styled-components/packages/styled-components/src/constructors/styled.ts ]。
import createStyledComponent from '../models/StyledComponent';
import { IStyledComponentFactory, WebTarget } from '../types';
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';
const styled = <Props>(tag: WebTarget) =>
constructWithOptions<IStyledComponentFactory, Props>(createStyledComponent, tag);
type BaseStyled = typeof styled;
const enhancedStyled = styled as BaseStyled &
{
[key in typeof domElements[number]]: ReturnType<BaseStyled>;
};
// 针对所有有效的 HTML 元素,支持快捷访问
// 即在项目中我们经常使用的 styled.button 等价于 styled('button')
domElements.forEach(domElement => {
enhancedStyled[domElement] = styled(domElement);
});
export default enhancedStyled;
import createStyledComponent from '../models/StyledComponent';
import { IStyledComponentFactory, WebTarget } from '../types';
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';
const styled = <Props>(tag: WebTarget) =>
constructWithOptions<IStyledComponentFactory, Props>(createStyledComponent, tag);
type BaseStyled = typeof styled;
const enhancedStyled = styled as BaseStyled &
{
[key in typeof domElements[number]]: ReturnType<BaseStyled>;
};
// 针对所有有效的 HTML 元素,支持快捷访问
// 即在项目中我们经常使用的 styled.button 等价于 styled('button')
domElements.forEach(domElement => {
enhancedStyled[domElement] = styled(domElement);
});
export default enhancedStyled;
styled 函数接受一个DOM结点 或者 组件作为入参,返回 constructWithOptions 函数的执行结果。constructWithOptions.ts 位于同级目录下面,总共有 50 多行代码。
export default function constructWithOptions<
Constructor extends Function = IStyledComponentFactory,
OuterProps = undefined // used for styled<{}>().attrs() so attrs() gets the generic prop context
>(componentConstructor: Constructor, tag: WebTarget, options: Options = EMPTY_OBJECT as Object) {
// We trust that the tag is a valid component as long as it isn't falsish
// Typically the tag here is a string or function (i.e. class or pure function component)
// However a component may also be an object if it uses another utility, e.g. React.memo
// React will output an appropriate warning however if the `tag` isn't valid
if (!tag) {
throw styledError(1, tag);
}
/* This is callable directly as a template function */
const templateFunction = <Props = OuterProps>(
initialStyles: TemplateStringsArray | StyledObject | StyleFunction<Props>,
...interpolations: Interpolation<Props>[]
) => componentConstructor(tag, options, css(initialStyles, ...interpolations));
/* Modify/inject new props at runtime */
templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props>) =>
constructWithOptions<Constructor, Props>(componentConstructor, tag, {
...options,
attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
});
/**
* If config methods are called, wrap up a new template function and merge options */
templateFunction.withConfig = (config: Options) =>
constructWithOptions<Constructor, OuterProps>(componentConstructor, tag, {
...options,
...config,
});
return templateFunction;
}
export default function constructWithOptions<
Constructor extends Function = IStyledComponentFactory,
OuterProps = undefined // used for styled<{}>().attrs() so attrs() gets the generic prop context
>(componentConstructor: Constructor, tag: WebTarget, options: Options = EMPTY_OBJECT as Object) {
// We trust that the tag is a valid component as long as it isn't falsish
// Typically the tag here is a string or function (i.e. class or pure function component)
// However a component may also be an object if it uses another utility, e.g. React.memo
// React will output an appropriate warning however if the `tag` isn't valid
if (!tag) {
throw styledError(1, tag);
}
/* This is callable directly as a template function */
const templateFunction = <Props = OuterProps>(
initialStyles: TemplateStringsArray | StyledObject | StyleFunction<Props>,
...interpolations: Interpolation<Props>[]
) => componentConstructor(tag, options, css(initialStyles, ...interpolations));
/* Modify/inject new props at runtime */
templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props>) =>
constructWithOptions<Constructor, Props>(componentConstructor, tag, {
...options,
attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
});
/**
* If config methods are called, wrap up a new template function and merge options */
templateFunction.withConfig = (config: Options) =>
constructWithOptions<Constructor, OuterProps>(componentConstructor, tag, {
...options,
...config,
});
return templateFunction;
}
templateFunction 函数把传入的 args 作为 css 内容,由 css-helper 生成为 style-sheet,然后调用 componentConstructor 生成一个SC实例。 而这里的 componentConstructor 函数是【styled-components/packages/styled-components/src/models/StyledComponent.ts】 文件默认导出的。主要是根据 tag 和 css 生成styled-component。
// 利用 forwardRef 创建一个新的临时组件,而不是拓展父级组件
let WrappedStyledComponent: IStyledComponent;
function forwardRef(props: ExtensibleObject, ref: Ref<Element>) {
// eslint-disable-next-line
return useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic);
}
forwardRef.displayName = displayName;
WrappedStyledComponent = React.forwardRef(forwardRef) as unknown as IStyledComponent;
WrappedStyledComponent.attrs = finalAttrs;
WrappedStyledComponent.componentStyle = componentStyle;
WrappedStyledComponent.displayName = displayName;
WrappedStyledComponent.shouldForwardProp = shouldForwardProp;
// this static is used to preserve the cascade of static classes for component selector
// purposes; this is especially important with usage of the css prop
WrappedStyledComponent.foldedComponentIds = isTargetStyledComp
? styledComponentTarget.foldedComponentIds.concat(styledComponentTarget.styledComponentId)
: (EMPTY_ARRAY as string[]);
WrappedStyledComponent.styledComponentId = styledComponentId;
// fold the underlying StyledComponent target up since we folded the styles
WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target;
// 生成样式类名
const generatedClassName = useInjectedStyle(
componentStyle,
isStatic,
context,
process.env. NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined
);
// 生成结点
propsForElement[
// handle custom elements which React doesn't properly alias
isTargetTag &&
domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string>) === -1
? 'class'
: 'className'
] = (foldedComponentIds as string[])
.concat(
styledComponentId,
(generatedClassName !== styledComponentId ? generatedClassName : null) as string,
props.className,
attrs.className
)
.filter(Boolean)
.join(' ');
propsForElement.ref = refToForward;
return createElement(elementToBeCreated, propsForElement);
// 利用 forwardRef 创建一个新的临时组件,而不是拓展父级组件
let WrappedStyledComponent: IStyledComponent;
function forwardRef(props: ExtensibleObject, ref: Ref<Element>) {
// eslint-disable-next-line
return useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic);
}
forwardRef.displayName = displayName;
WrappedStyledComponent = React.forwardRef(forwardRef) as unknown as IStyledComponent;
WrappedStyledComponent.attrs = finalAttrs;
WrappedStyledComponent.componentStyle = componentStyle;
WrappedStyledComponent.displayName = displayName;
WrappedStyledComponent.shouldForwardProp = shouldForwardProp;
// this static is used to preserve the cascade of static classes for component selector
// purposes; this is especially important with usage of the css prop
WrappedStyledComponent.foldedComponentIds = isTargetStyledComp
? styledComponentTarget.foldedComponentIds.concat(styledComponentTarget.styledComponentId)
: (EMPTY_ARRAY as string[]);
WrappedStyledComponent.styledComponentId = styledComponentId;
// fold the underlying StyledComponent target up since we folded the styles
WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target;
// 生成样式类名
const generatedClassName = useInjectedStyle(
componentStyle,
isStatic,
context,
process.env. NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined
);
// 生成结点
propsForElement[
// handle custom elements which React doesn't properly alias
isTargetTag &&
domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string>) === -1
? 'class'
: 'className'
] = (foldedComponentIds as string[])
.concat(
styledComponentId,
(generatedClassName !== styledComponentId ? generatedClassName : null) as string,
props.className,
attrs.className
)
.filter(Boolean)
.join(' ');
propsForElement.ref = refToForward;
return createElement(elementToBeCreated, propsForElement);
如上所示,我们选取了StyledComponent.ts中的关键代码片段。首先根据传入的参数创建了一个临时包装组件,然后经过对 target 属性的处理和组合,生成关系样式表的动态样式类,最后实例化组件。 总结一下:
- 我们利用 es6中的标签模板字符串,可以获得样式代码,以及传递的属性值等信息
- 传递 target 参数,经过多层预处理和 组合,包装成一个临时组件
- 生产样式类数组,并关联样式代码
- 最后,将组件实例化,交给 react
如果 target 也是SC,则包含了自己和target的样式类名,起到了继承的效果
高级拓展
主题切换
import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
color: ${( props ) => props.theme.color.primary}
`
const theme = {
color: {
primary: 'red'
}
}
class StyledComponentsDemo extends Component {
render() {
return(
<ThemeProvider theme={theme}>
<Item>Item 1</Item>
</ThemeProvider>
)
}
}
import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
color: ${( props ) => props.theme.color.primary}
`
const theme = {
color: {
primary: 'red'
}
}
class StyledComponentsDemo extends Component {
render() {
return(
<ThemeProvider theme={theme}>
<Item>Item 1</Item>
</ThemeProvider>
)
}
}
通过暴露的ThemeProvider传递theme属性,实现不同主题效果的切换。普通的组件也可以通过提供的 withTheme 函数来访问主题属性
import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
render() {
return <p>{this.props.theme.color.primary}</p>
}
}
export default withTheme(MyComponent);
import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
render() {
return <p>{this.props.theme.color.primary}</p>
}
}
export default withTheme(MyComponent);
VS-Code插件
在VS-Code中书写样式组件的CSS代码时,默认没有提示,且没有样式属性的高亮展示,这一点不是很友好。 为了获得更好的代码书写体验,提升开发效率,这里推荐一个 VS-Code插件:vscode-styled-components。它支持 styled-components 的高亮展示 和 CSS语法智能提示。 效果对比很明显。
相关参考
https://styled-components.com/https://github.com/styled-components/styled-componentshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literalshttps://rangle.io/blog/styled-components-styled-systems-and-how-they-work/https://juejin.cn/post/6844904196425121800https://github.com/wangpin34/blog/issues/49